package com.iwhalecloud.citybrain.flow.platform.manager.application.utils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;

import com.iwhalecloud.citybrain.flow.platform.manager.application.exceptions.FlowGraphTravelException;
import lombok.Data;
import org.flowable.bpmn.model.Event;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.FlowNode;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.SequenceFlow;
import org.flowable.bpmn.model.UserTask;

/**
 * visitor模式用于遍历流程图
 * @author lich
 * @date 2021/11/10
 */
public class FlowGraphTraveller {

    private static final String START_EVENT_ID = "start";

    private Process mainProcess;

    private List<UserTask> firstUserTasks;

    public FlowGraphTraveller(Process mainProcess) {
        this.mainProcess = mainProcess;
    }

    public List<UserTask> findFirstUserTasks() {
        if (firstUserTasks != null) {
            return firstUserTasks;
        }
        firstUserTasks = new ArrayList<>();
        traverseForward(START_EVENT_ID, param -> {
            firstUserTasks.add(param.getUserTask());
        }, this::beyondFirstUserTask);

        return firstUserTasks;
    }

    private boolean beyondFirstUserTask(VisitorParam param) {
        return param.getUserTask() != null && param.getPathCount().getUserTask() > 1;
    }

    /**
     * 从当前节点开始向后遍历
     * @param currentElementId 开始节点id
     * @param visitor 每次遇到一个UserTask如何处理
     * @param stopWhen 结束遍历的条件
     */
    public void traverseBackward(String currentElementId, Consumer<VisitorParam> visitor,
        Predicate<VisitorParam> stopWhen) {
        traverseWithDirection(currentElementId, visitor, stopWhen, true);
    }

    /**
     * 从当前节点开始向前遍历
     * @param currentElementId 开始节点id
     * @param visitor 每次遇到一个UserTask如何处理
     * @param stopWhen 结束遍历的条件
     */
    public void traverseForward(String currentElementId, Consumer<VisitorParam> visitor,
        Predicate<VisitorParam> stopWhen) {
        traverseWithDirection(currentElementId, visitor, stopWhen, false);
    }

    private void traverseWithDirection(String currentElementId, Consumer<VisitorParam> visitor,
        Predicate<VisitorParam> stopWhen, boolean backward) {
        FlowElement currentElement = mainProcess.getFlowElement(currentElementId);
        if (currentElement == null) {
            throw new FlowGraphTravelException(
                String.format("在流程%s中，没有查询到id为%s的节点", mainProcess.getName(), currentElementId));
        }

        TraverseParam param = new TraverseParam();
        param.setStopWhen(stopWhen);
        param.setVisitor(visitor);
        param.setVisited(new HashSet<>());

        traverseInternal(currentElement, param, backward);
    }


    private void traverseInternal(FlowElement currentElement, TraverseParam traverseParam, boolean backward) {
        if (currentElement == null || traverseParam.visited.contains(currentElement.getId())) {
            return;
        }

        traverseParam.visited.add(currentElement.getId());
        // 如果是连线
        if (currentElement instanceof SequenceFlow) {
            FlowElement destElement;
            if (backward) {
                //如果是向后看，则获取来源节点
                destElement = ((SequenceFlow) currentElement).getSourceFlowElement();
            } else {
                //如果是向前看，则获取目标节点
                destElement = ((SequenceFlow) currentElement).getTargetFlowElement();
            }

            addPath(traverseParam, currentElement, destElement);
            traverseInternal(destElement, traverseParam, backward);
        }

        if (currentElement instanceof FlowNode) {

            VisitorParam visitorParam = new VisitorParam();
            visitorParam.setFlowElement(currentElement);
            if (!traverseParam.getPathStats().containsKey(currentElement.getId())) {
                traverseParam.getPathStats().put(currentElement.getId(), new PathCount());
            }
            visitorParam.setPathCount(traverseParam.getPathStats().get(currentElement.getId()));
            visitorParam.setUserTask(currentElement instanceof UserTask ? (UserTask)currentElement : null);

            if (traverseParam.stopWhen != null && traverseParam.stopWhen.test(visitorParam)) {
                return;
            }

            //1.如果是任务节点
            if (currentElement instanceof UserTask) {
                //1.1 访问该任务节点
                traverseParam.visitor.accept(visitorParam);
            }

            //2.该任务节点的来源向上或者向下追溯
            List<SequenceFlow> nextFlows = backward ?
                ((FlowNode) currentElement).getIncomingFlows() :
                ((FlowNode) currentElement).getOutgoingFlows();
            for (SequenceFlow sf : nextFlows) {
                addPath(traverseParam, currentElement, sf);
                traverseInternal(sf, traverseParam, backward);
            }

        }
    }

    // 主要用于生成路径，给定某一个节点，得知其与目标节点的距离
    private void addPath(TraverseParam param, FlowElement currentElement, FlowElement nextElement) {
        Map<String, PathCount> pathStats = param.getPathStats();
        if (!pathStats.containsKey(currentElement.getId())) {
            pathStats.put(currentElement.getId(), new PathCount());
        }
        PathCount pathCount = pathStats.get(currentElement.getId());

        PathCount newPathCount = new PathCount();
        newPathCount.setTotal(pathCount.getTotal() + 1);
        newPathCount.setUserTask(nextElement instanceof UserTask ? pathCount.getUserTask() + 1 : pathCount.getUserTask());

        pathStats.put(nextElement.getId(), newPathCount);
    }

    @Data
    static class TraverseParam {

        private Consumer<VisitorParam> visitor;

        private Predicate<VisitorParam> stopWhen;

        private Set<String> visited;

        private Map<String, PathCount> pathStats = new HashMap<>();

    }

    @Data
    static class PathCount {

        int userTask;

        int total;
    }

    @Data
    public static class VisitorParam {

        private PathCount pathCount;

        private FlowElement flowElement;

        private UserTask userTask;
    }

}
